#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# QcmCalculator Copyright (C) 2009  Chris Marsh
# Version 0.2.0
#
# QcmCalculator is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or any later version.
#
# This program is distributed in the hope that it will be useful, 
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# ********************************************************************
# Based on C++ code from the Qt Toolkit documentation.
# Copyright (C) 2005-2005 Trolltech AS.
# ********************************************************************

from PyQt4.QtCore import *
from PyQt4.QtGui import *
import QcmCalculator_rc

# ********************************************************************

class CalculatorButton(QToolButton):
    """ Define a square QToolButton."""
    def __init__(self, text,  parent=None):
        """Constructor sets the size policy and button text."""
        super(CalculatorButton, self).__init__(parent)
        self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
        self.setText(text)
    # __init__
    
    def sizeHint(self):
        """Return a minimum square size."""
        #TODO: Returning a square button affects the resizing,
        # sizeHint needs changed to something else. Removing the sizeHint
        # results in some buttons wider than others due to wider text content.
        # Need method to return consistant size to keep all buttons the same
        # regardless of content.
        size = QToolButton.sizeHint(self)
        size.setHeight(min(size.height(),  size.width()))
        size.setWidth(min(size.height(),  size.width()))
        return size
    # sizeHint
 
# ********************************************************************

class CalculatorDisplay(QLineEdit):
    """ Calculator display with built-in operator label as suffix"""
    
    styleSheetTemplate = """
        QLineEdit#calculatorDisplay {
            color: white;
            background-color: black;
            padding: 4px %spx 4px 4px}
        QLabel#operatorLabel {
            color: white;}
        """
    def __init__(self, parent=None):
        """Constructor sets the size policy and creates the operator suffix label."""
        super(CalculatorDisplay, self).__init__(parent)
        self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
        
        self.operatorLabel = QLabel(self)
        self.operatorLabel.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
        self.operatorLabel.setObjectName("operatorLabel")
        labelFont = self.operatorLabel.font()
        self.setLabelFontSize()
    # __init__
    
    def applyStyleSheet(self):
        """Style sheet adds right padding to create space for the label."""
        padding = self.operatorLabel.fontMetrics().height()
        ss = CalculatorDisplay.styleSheetTemplate % (padding)
        self.setStyleSheet(ss)
    
    def setOperator(self,  operator):
        """Covenience method to set operator label text."""
        self.operatorLabel.setText(operator)
    # setOperator
    
    def setFontSize(self,  pointSize):
        """Set font size and adjust label font accordingly."""
        font = self.font()
        font.setPointSize(pointSize)
        self.setFont(font)
        self.setLabelFontSize()
    # setFontSize
    
    def setLabelFontSize(self):
        """Set label font to percentage (50%) of the QLineEdit font"""
        font = self.operatorLabel.font()
        font.setPointSize(self.font().pointSize()*0.5)
        self.operatorLabel.setFont(font)
    # setLabelFontSize

    def resizeEvent(self,  event):
        """Set the size of the child label.
        The label size is set to same as its parent, but reducing the width,
        to allow a margin on the right-aligned side."""
        frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
        self.operatorLabel.setFixedSize(self.width()-(frameWidth*2)-4,  self.height())
        self.applyStyleSheet()
    # resizeEvent
    
    def sizeHint(self):
        """Return a square size based on the width"""
        size = QLineEdit.sizeHint(self)
        size.setHeight(self.fontMetrics().height() + 4)
        return size
    # sizeHint
    
# ********************************************************************
 
class QcmCalculator(QFrame):
    """Define the calculator gui.
    The gui can be used indepently as a calculator application, but is
    primarily designed for use as a popup widget. It is intended for
    use with QcmCalculatorEdit which is a QLineEdit with a button to
    show the calculator.
    When a calculation is complete the result is given back to the
    calling parent (QcmCalculatorEdit) for display.
    """
    Popup = True
    NumDigitButtons = 10
    ChangeSign = "\261"
    Division = "\367"
    Times = "\327"

    styleSheet = """
        CalculatorButton#digitButton {
            color: black;
            background-color: #BFD9FF;}
        CalculatorButton#clearButton {
            color: white;
            background-color: #E85752;}
        CalculatorButton#operatorButton {
            color: black;
            background-color: #C3B5DA;}
        CalculatorButton#memoryButton {
            color: black;
            background-color: #80B3FF;}
        """

    def __init__(self, isPopup=False,  parent = None):
        """Initialize the calculator variables and gui widgets.
        sumInMemory: Total in memory for recall.
        sumSoFar: Current addition result in current calculation.
        factorSoFar: Current Multiplicative result in current calculation.
        waitingForOperand: Boolean, true if waiting for number input.
        pendingMultiplicativeOperator: Contains '*' or '/'.
        pendingAdditiveOperator: Contains '+' or '-'.
        """
        super(QcmCalculator, self).__init__(parent)
        
        self.__deleteOnClose = False
        
        self.setStyleSheet(QcmCalculator.styleSheet)
        self.sumInMemory = 0.0
        self.sumSoFar = 0.0
        self.factorSoFar = 0.0
        self.waitingForOperand = True
        self.pendingAdditiveOperator = QString()
        self.pendingMultiplicativeOperator = QString()

        self.display = CalculatorDisplay()
        self.display.setObjectName("calculatorDisplay")
        self.display.setReadOnly(True)
        self.display.setAlignment(Qt.AlignRight)
        self.display.setMaxLength(15)

        displayFont = self.display.font()
        displayFont.setPointSize(displayFont.pointSize() + 4);
        self.display.setFont(displayFont)
        self.display.setText("0")
        
        digitButtons = []
        for i in range(QcmCalculator.NumDigitButtons):
            digitButtons.append(self.createButton("digitButton",  QString.number(i), self.digitClicked))

        pointButton = self.createButton("digitButton",  self.tr("."), self.pointClicked)
        changeSignButton = self.createButton("operatorButton",  self.tr(QcmCalculator.ChangeSign), self.changeSignClicked)

        self.backspaceButton = self.createButton("operatorButton",  "", self.backspaceClicked)
        self.backspaceButton.setIcon(QIcon(QPixmap(":/clear.png")))
        clearButton = self.createButton("clearButton",  self.tr("C"), self.clear)
        clearAllButton = self.createButton("clearButton",  self.tr("CA"), self.clearAll)

        clearMemoryButton = self.createButton("memoryButton",  self.tr("MC"), self.clearMemory)
        readMemoryButton = self.createButton("memoryButton",  self.tr("MR"), self.readMemory)
        setMemoryButton = self.createButton("memoryButton",  self.tr("MS"), self.setMemory)
        addToMemoryButton = self.createButton("memoryButton",  self.tr("M+"), self.addToMemory)

        divisionButton = self.createButton("operatorButton",  self.tr(QcmCalculator.Division), self.multiplicativeOperatorClicked)
        timesButton = self.createButton("operatorButton",  self.tr(QcmCalculator.Times), self.multiplicativeOperatorClicked)
        minusButton = self.createButton("operatorButton",  self.tr("-"), self.additiveOperatorClicked)
        plusButton = self.createButton("operatorButton",  self.tr("+"), self.additiveOperatorClicked)
        percentButton = self.createButton("operatorButton",  self.tr("%"),  self.percentOperatorClicked) 
        
        okButton = self.createButton("operatorButton",  "",  self.acceptClicked)
        okButton.setIcon(QIcon(QPixmap(":/apply.png")))
        cancelButton = self.createButton("operatorButton",  "",  self.close)
        cancelButton.setIcon(QIcon(QPixmap(":/cancel.png")))
        
        equalButton = self.createButton("operatorButton",  self.tr("="), self.equalClicked)

        mainLayout = QGridLayout()
        mainLayout.setSpacing(0)
        mainLayout.setMargin(0)
        
        mainLayout.addWidget(self.display, 0, 0, 1, 6)
        mainLayout.addWidget(changeSignButton, 2, 0)
        mainLayout.addWidget(percentButton,  3,  0)
        mainLayout.addWidget(clearButton, 4, 0)
        mainLayout.addWidget(clearAllButton, 5, 0)
        mainLayout.addWidget(clearMemoryButton, 1, 0)
        mainLayout.addWidget(readMemoryButton, 1, 1)
        mainLayout.addWidget(setMemoryButton, 1, 2)
        mainLayout.addWidget(addToMemoryButton, 1, 3)

        for i in range(1, QcmCalculator.NumDigitButtons):
             row = ((9 - i) / 3) + 2
             column = ((i - 1) % 3) + 1
             mainLayout.addWidget(digitButtons[i], row, column)

        mainLayout.addWidget(digitButtons[0], 5, 1)
        mainLayout.addWidget(pointButton, 5, 2)
        mainLayout.addWidget(equalButton, 5, 3)
        mainLayout.addWidget(divisionButton, 1, 4)
        mainLayout.addWidget(timesButton, 2, 4)
        mainLayout.addWidget(minusButton, 3, 4)
        mainLayout.addWidget(plusButton, 4, 4,  2,  1)
        
        if isPopup:
            mainLayout.addWidget(self.backspaceButton, 1, 5)
            mainLayout.addWidget(okButton,  2,   5,  3,  1)
            mainLayout.addWidget(cancelButton,  5,  5)
            self.setWindowFlags(Qt.Popup)
            self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        self.setLayout(mainLayout)
        
        self.setWindowTitle(self.tr("Calculator"))
    # __init__

    def closeEvent(self,  event):
        """The calculator window can be set to delete itself when closed.
        The hope is to reduce the memory footprint when used a child
        popup and many instances are used within an application.
        """
        if self.__deleteOnClose:
            self.deleteLater()
        event.accept()
    # closeEvent
    
    def deleteOnClose(self):
        """Returns the boolean status of the widgets close action."""
        return self.__deleteOnClose
    # deleteOnClose
    
    def setDeleteOnClose(self,  deleteOnClose):
        """Sets the boolean status of the widgets close action."""
        self.__deleteOnClose = deleteOnClose
    # setDeleteOnClose
    
    def resizeEvent(self,  event):
        """Changes the size of font on the widgets child controls.
        The font size is changed according to the current window
        height. This allows the calculator interface to scale.
        """
        children = self.children()
        for child in children:
            try:
                if QGridLayout != type(child):
                    if child.objectName() == "calculatorDisplay":
                        child.setFontSize(self.height()*0.08)
                    else:
                        font = child.font()
                        font.setPointSize(min(self.height(),  self.width())*0.05)
                        child.setFont(font)
            except:
                print "Error setting font size on: %s" % type(child)
    # resizeEvent
    
    def keyPressEvent(self,  event):
        """Implements the keyboard interface for the calculator.
        The key pressed is match to the calculator button text and
        an animated click called, which fires the buttons clicked signals.
        """
        if event.key() != Qt.Key_Backspace and event.text() == "": return
        if event.key() == Qt.Key_Asterisk:
            text = self.tr(QcmCalculator.Times)
        elif event.key() == Qt.Key_Slash:
            text = self.tr(QcmCalculator.Division)
        else:
            text = event.text().toUpper()
        
        if event.key() == Qt.Key_Backspace:
            self.backspaceButton.animateClick()
        else:
            children = self.children()
            for child in children:
                if CalculatorButton == type(child):
                    if text == child.text():
                        child.animateClick()
                        return
    # keyPressEvent

    def createButton(self, name,  text, slot):
        """ Creates calculator button.
        name - sets the objectName. The objectName is used for defining the buttons
        function group. The calculators Style Sheet sets the colors of the buttons according
        to the objectName.
        text - text for the button top
        slot - the slot to be called when the button is clicked
        """
        button = CalculatorButton(text)
        button.setObjectName(name)
        self.connect(button, SIGNAL("clicked()"), slot)
        return button
    # createButton
    
    def acceptClicked(self):
        """ Called when the  the accept button (tick icon) is clicked.
        A signal is fired giving the calculation result as a text string.
        """
        self.emit(SIGNAL("finishedCalculating(QString&)"),  (self.display.text()))
        self.close()
    # acceptClicked

    def digitClicked(self):
        """ Called when a numerical button is clicked.
        The button is identified by its text, ie '1', '2', etc and the
        corresponding number is added to the calculator display.
        """
        clickedButton = self.sender()
        digitValue = clickedButton.text().toInt()[0]
        if (self.display.text() == "0" and digitValue == 0.0):
             return

        if self.waitingForOperand:
             self.display.clear()
             self.waitingForOperand = False

        self.display.setText(self.display.text() + QString.number(digitValue))
    # digitClicked 
    
    def additiveOperatorClicked(self):
        """ Called when '+' or '-' buttons are clicked.
        The operator is taken from the buttons text.
        Before processing the addition, the method checks if any multiplication
        operations are in progress, ie '2*3+...', and processes the multiplication
        to obtain a new result before proceeding with the addition.
        """
        clickedButton = self.sender()
        clickedOperator = clickedButton.text()
        operand = self.display.text().toDouble()[0]

        if not self.pendingMultiplicativeOperator.isEmpty():
            if not self.calculate(operand, self.pendingMultiplicativeOperator):
                self.abortOperation()
                return
            self.display.setText(QString.number(self.factorSoFar))
            operand = self.factorSoFar
            factorSoFar = 0.0
            self.pendingMultiplicativeOperator.clear()

        if not self.pendingAdditiveOperator.isEmpty():
            if not self.calculate(operand, self.pendingAdditiveOperator):
                self.abortOperation()
                return
            self.display.setText(QString.number(self.sumSoFar))
        else:
            self.sumSoFar = operand

        self.pendingAdditiveOperator = clickedOperator
        self.display.setOperator(clickedOperator)
        self.waitingForOperand = True
    # additiveOperatorClicked

    def multiplicativeOperatorClicked(self):
        clickedButton = self.sender()
        clickedOperator = clickedButton.text()
        operand = self.display.text().toDouble()[0]
        
        if not self.pendingMultiplicativeOperator.isEmpty():
            if not self.calculate(operand, self.pendingMultiplicativeOperator):
                self.abortOperation()
                return
            self.display.setText(QString.number(self.factorSoFar))
        else:
            self.factorSoFar = operand

        self.pendingMultiplicativeOperator = clickedOperator
        self.display.setOperator(clickedOperator)
        self.waitingForOperand = True
    # multiplicativeOperatorClicked
    
    def percentOperatorClicked(self):
        operand = self.display.text().toDouble()[0]

        if not self.pendingMultiplicativeOperator.isEmpty():
            operand /= 100
            if not self.calculate(operand, self.pendingMultiplicativeOperator):
                self.abortOperation()
                return
            operand = self.factorSoFar
            factorSoFar = 0.0
            self.pendingMultiplicativeOperator.clear()

        if not self.pendingAdditiveOperator.isEmpty():
            operand *= self.sumSoFar / 100
        if not self.pendingAdditiveOperator.isEmpty():
            if not self.calculate(operand, self.pendingAdditiveOperator):
                self.abortOperation()
                return
            self.pendingAdditiveOperator.clear()
        else:
            self.sumSoFar = operand

        self.display.setText(QString.number(self.sumSoFar))
        self.sumSoFar = 0.0
        self.waitingForOperand = True
        self.display.setOperator("")
    # percentOperatorClicked

    def equalClicked(self):
        operand = self.display.text().toDouble()[0]
        
        if not self.pendingMultiplicativeOperator.isEmpty():
            if not self.calculate(operand, self.pendingMultiplicativeOperator):
                self.abortOperation()
                return
            operand = self.factorSoFar
            self.factorSoFar = 0.0
            self.pendingMultiplicativeOperator.clear()
        
        if not self.pendingAdditiveOperator.isEmpty():
            if not self.calculate(operand, self.pendingAdditiveOperator):
                self.abortOperation()
                return
            self.pendingAdditiveOperator.clear()
        else:
            self.sumSoFar = operand

        self.display.setText(QString.number(self.sumSoFar))
        self.sumSoFar = 0.0
        self.waitingForOperand = True
        self.display.setOperator("")
    # equalClicked

    def pointClicked(self):
        if self.waitingForOperand:
            self.display.setText("0")
        if not self.display.text().contains("."):
            self.display.setText(self.display.text() + self.tr("."))
        self.waitingForOperand = False
    # pointClicked

    def changeSignClicked(self):
         text = self.display.text()
         value = text.toDouble()[0]
         if value > 0.0:
             text.prepend(self.tr("-"))
         elif value < 0.0:
             text.remove(0, 1)

         self.display.setText(text)
    # changeSignClicked

    def backspaceClicked(self):
        if self.waitingForOperand:
            return
        text = self.display.text()
        text.chop(1)
        if text.isEmpty():
            text = "0"
            self.waitingForOperand = True
        self.display.setText(text)
    # backspaceClicked

    def clear(self):
         if self.waitingForOperand:
             return
         self.display.setText("0")
         self.waitingForOperand = True
    # clear

    def clearAll(self):
        self.sumSoFar = 0.0
        self.factorSoFar = 0.0
        self.pendingAdditiveOperator.clear()
        self.pendingMultiplicativeOperator.clear()
        self.display.setText("0")
        self.waitingForOperand = True
        self.display.setOperator("")
    # clearAll

    def clearMemory(self):
         self.sumInMemory = 0.0
    # clearMemory

    def readMemory(self):
         self.display.setText(QString.number(self.sumInMemory));
         self.waitingForOperand = True
    # readMemory

    def setMemory(self):
         self.equalClicked()
         self.sumInMemory = self.display.text().toDouble()[0]
    # setMemory

    def addToMemory(self):
         self.equalClicked()
         self.sumInMemory += self.display.text().toDouble()[0]
    # addToMemory

    def abortOperation(self):
         self.clearAll()
         self.display.setText(self.tr("####"))
    # abortOperation

    def calculate(self, rightOperand, pendingOperator):
        if pendingOperator == self.tr("+"):
            self.sumSoFar += rightOperand
        elif pendingOperator == self.tr("-"):
            self.sumSoFar -= rightOperand
        elif pendingOperator == self.tr(QcmCalculator.Times):
            self.factorSoFar *= rightOperand
        elif pendingOperator == self.tr(QcmCalculator.Division):
            if rightOperand == 0.0:
                return False
            self.factorSoFar /= rightOperand
        return True
    # calculate

# ********************************************************************

if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    calculator = QcmCalculator()
    calculator.show()
    sys.exit(app.exec_())
